// CHECKSTYLE_OFF: Copyrighted to ASF
/*
 * Copyright 1999-2004 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
// CHECKSTYLE_ON
package com.google.j2cl.jre.java.util;

import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
 * Tests base {@link java.util.List} methods and contracts.
 *
 * <p>To use, simply extend this class, and implement the {@link #makeList} method.
 *
 * <p>If your {@link List} fails one of these tests by design, you may still use this base set of
 * cases. Simply override the test case (method) your {@link List} fails.
 */
@SuppressWarnings({"unchecked", "rawtypes"})
@NullMarked
abstract class TestList extends TestCollection {

  /**
   * Return a new, empty {@link List} to be used for testing.
   *
   * @return an empty list for testing.
   */
  protected abstract List<@Nullable Object> makeEmptyList();

  /**
   * Return a new, full {@link List} to be used for testing.
   *
   * @return a full list for testing
   */
  protected List<@Nullable Object> makeFullList() {
    // only works if list supports optional "addAll(Collection)"
    List list = makeEmptyList();
    list.addAll(Arrays.asList(getFullElements()));
    return list;
  }

  /**
   * Returns {@link makeEmptyList()}.
   *
   * @return an empty list to be used for testing
   */
  @Override
  protected final Collection<@Nullable Object> makeCollection() {
    return makeEmptyList();
  }

  /**
   * Returns {@link makeFullList()}.
   *
   * @return a full list to be used for testing
   */
  @Override
  protected final Collection<@Nullable Object> makeFullCollection() {
    return makeFullList();
  }

  /**
   * Whether or not we are testing an iterator that can be empty. Default is true.
   *
   * @return true if Iterators can be empty
   */
  public boolean supportsEmptyIterator() {
    return true;
  }

  /**
   * Whether or not we are testing an list that allows element set. Default is true.
   *
   * @return true if Lists support element set
   */
  public boolean isSetSupported() {
    return true;
  }

  /**
   * Returns the {@link collection} field cast to a {@link List}.
   *
   * @return the collection field as a List
   */
  protected List<@Nullable Object> getList() {
    return (List<@Nullable Object>) collection;
  }

  /**
   * Returns the {@link confirmed} field cast to a {@link List}.
   *
   * @return the confirmed field as a List
   */
  protected List<@Nullable Object> getConfirmedList() {
    return (List<@Nullable Object>) confirmed;
  }

  public class TestListIterator extends AbstractTestListIterator {

    @Override
    public Object addSetValue() {
      return TestList.this.getOtherElements()[0];
    }

    @Override
    public boolean supportsRemove() {
      return TestList.this.isRemoveSupported();
    }

    @Override
    public boolean supportsAdd() {
      return TestList.this.isAddSupported();
    }

    @Override
    public boolean supportsSet() {
      return TestList.this.isSetSupported();
    }

    @Override
    public ListIterator makeEmptyListIterator() {
      resetEmpty();
      return ((List) TestList.this.collection).listIterator();
    }

    @Override
    public ListIterator makeFullListIterator() {
      resetFull();
      return ((List) TestList.this.collection).listIterator();
    }

    @Override
    public Object makeObject() {
      return ((List) TestList.this.collection).listIterator();
    }
  }

  /** Tests the read-only bits of {@link List#listIterator()}. */
  public void testListListIterator() {
    resetFull();
    forwardTest(getList().listIterator(), 0);
    backwardTest(getList().listIterator(), 0);
  }

  /** Tests the read-only bits of {@link List#listIterator(int)}. */
  public void testListListIteratorByIndex() {
    resetFull();
    try {
      getList().listIterator(-1);
    } catch (IndexOutOfBoundsException ex) {
    }
    resetFull();
    try {
      getList().listIterator(getList().size() + 1);
    } catch (IndexOutOfBoundsException ex) {
    }
    resetFull();
    for (int i = 0; i <= confirmed.size(); i++) {
      forwardTest(getList().listIterator(i), i);
      backwardTest(getList().listIterator(i), i);
    }
    resetFull();
    for (int i = 0; i <= confirmed.size(); i++) {
      backwardTest(getList().listIterator(i), i);
    }
  }

  // -----------------------------------------------------------------------
  /** Tests remove on list iterator is correct. */
  public void testListListIteratorPreviousRemoveNext() {
    if (!isRemoveSupported()) {
      return;
    }
    resetFull();
    if (collection.size() < 4) {
      return;
    }
    ListIterator it = getList().listIterator();
    Object zero = it.next();
    Object one = it.next();
    Object two = it.next();
    Object two2 = it.previous();
    Object one2 = it.previous();
    assertEquals(one, one2);
    assertEquals(two, two2);
    assertEquals(zero, getList().get(0));
    assertEquals(one, getList().get(1));
    assertEquals(two, getList().get(2));

    it.remove(); // removed element at index 1 (one)
    assertEquals(zero, getList().get(0));
    assertEquals(two, getList().get(1));
    Object two3 = it.next(); // do next after remove
    assertEquals(two, two3);
    assertEquals(collection.size() > 2, it.hasNext());
    assertTrue(it.hasPrevious());
  }

  /** Tests remove on list iterator is correct. */
  public void testListListIteratorPreviousRemovePrevious() {
    if (!isRemoveSupported()) {
      return;
    }
    resetFull();
    if (collection.size() < 4) {
      return;
    }
    ListIterator it = getList().listIterator();
    Object zero = it.next();
    Object one = it.next();
    Object two = it.next();
    Object two2 = it.previous();
    Object one2 = it.previous();
    assertEquals(one, one2);
    assertEquals(two, two2);
    assertEquals(zero, getList().get(0));
    assertEquals(one, getList().get(1));
    assertEquals(two, getList().get(2));

    it.remove(); // removed element at index 1 (one)
    assertEquals(zero, getList().get(0));
    assertEquals(two, getList().get(1));
    Object zero3 = it.previous(); // do previous after remove
    assertEquals(zero, zero3);
    assertEquals(false, it.hasPrevious());
    assertEquals(collection.size() > 2, it.hasNext());
  }

  /** Tests remove on list iterator is correct. */
  public void testListListIteratorNextRemoveNext() {
    if (!isRemoveSupported()) {
      return;
    }
    resetFull();
    if (collection.size() < 4) {
      return;
    }
    ListIterator it = getList().listIterator();
    Object zero = it.next();
    Object one = it.next();
    Object two = it.next();
    assertEquals(zero, getList().get(0));
    assertEquals(one, getList().get(1));
    assertEquals(two, getList().get(2));
    Object three = getList().get(3);

    it.remove(); // removed element at index 2 (two)
    assertEquals(zero, getList().get(0));
    assertEquals(one, getList().get(1));
    Object three2 = it.next(); // do next after remove
    assertEquals(three, three2);
    assertEquals(collection.size() > 3, it.hasNext());
    assertEquals(true, it.hasPrevious());
  }

  /** Tests remove on list iterator is correct. */
  public void testListListIteratorNextRemovePrevious() {
    if (!isRemoveSupported()) {
      return;
    }
    resetFull();
    if (collection.size() < 4) {
      return;
    }
    ListIterator it = getList().listIterator();
    Object zero = it.next();
    Object one = it.next();
    Object two = it.next();
    assertEquals(zero, getList().get(0));
    assertEquals(one, getList().get(1));
    assertEquals(two, getList().get(2));

    it.remove(); // removed element at index 2 (two)
    assertEquals(zero, getList().get(0));
    assertEquals(one, getList().get(1));
    Object one2 = it.previous(); // do previous after remove
    assertEquals(one, one2);
    assertEquals(true, it.hasNext());
    assertEquals(true, it.hasPrevious());
  }

  // -----------------------------------------------------------------------
  /**
   * Traverses to the end of the given iterator.
   *
   * @param iter the iterator to traverse
   * @param i the starting index
   */
  private void forwardTest(ListIterator iter, int i) {
    List list = getList();
    int max = getFullElements().length;

    while (i < max) {
      assertTrue("Iterator should have next", iter.hasNext());
      assertEquals("Iterator.nextIndex should work", iter.nextIndex(), i);
      assertEquals("Iterator.previousIndex should work", iter.previousIndex(), i - 1);
      Object o = iter.next();
      assertEquals("Iterator returned correct element f", list.get(i), o);
      i++;
    }

    assertTrue("Iterator shouldn't have next", !iter.hasNext());
    assertEquals("nextIndex should be size", iter.nextIndex(), max);
    assertEquals("previousIndex should be size - 1", iter.previousIndex(), max - 1);

    try {
      iter.next();
      fail("Exhausted iterator should raise NoSuchElement");
    } catch (NoSuchElementException e) {
      // expected
    }
  }

  /**
   * Traverses to the beginning of the given iterator.
   *
   * @param iter the iterator to traverse
   * @param i the starting index
   */
  private void backwardTest(ListIterator iter, int i) {
    List list = getList();

    while (i > 0) {
      assertTrue("Iterator should have previous, i:" + i, iter.hasPrevious());
      assertEquals("Iterator.nextIndex should work, i:" + i, iter.nextIndex(), i);
      assertEquals("Iterator.previousIndex should work, i:" + i, iter.previousIndex(), i - 1);
      Object o = iter.previous();
      assertEquals("Iterator returned correct element b", list.get(i - 1), o);
      i--;
    }

    assertTrue("Iterator shouldn't have previous", !iter.hasPrevious());
    int nextIndex = iter.nextIndex();
    assertEquals("nextIndex should be 0, actual value: " + nextIndex, 0, nextIndex);
    int prevIndex = iter.previousIndex();
    assertEquals("previousIndex should be -1, actual value: " + prevIndex, -1, prevIndex);

    try {
      iter.previous();
      fail("Exhausted iterator should raise NoSuchElement");
    } catch (NoSuchElementException e) {
      // expected
    }
  }

  /** Tests the {@link ListIterator#add(Object)} method of the list iterator. */
  public void testListIteratorAdd() {
    if (!isAddSupported()) {
      return;
    }

    resetEmpty();
    List list1 = getList();
    List list2 = getConfirmedList();

    Object[] elements = getFullElements();
    ListIterator iter1 = list1.listIterator();
    ListIterator iter2 = list2.listIterator();

    for (int i = 0; i < elements.length; i++) {
      iter1.add(elements[i]);
      iter2.add(elements[i]);
      verify();
    }

    resetFull();
    iter1 = getList().listIterator();
    iter2 = getConfirmedList().listIterator();
    for (int i = 0; i < elements.length; i++) {
      iter1.next();
      iter2.next();
      iter1.add(elements[i]);
      iter2.add(elements[i]);
      verify();
    }
  }

  /** Tests the {@link ListIterator#set(Object)} method of the list iterator. */
  public void testListIteratorSet() {
    if (!isSetSupported()) {
      return;
    }

    Object[] elements = getFullElements();

    resetFull();
    ListIterator iter1 = getList().listIterator();
    ListIterator iter2 = getConfirmedList().listIterator();
    for (int i = 0; i < elements.length; i++) {
      iter1.next();
      iter2.next();
      iter1.set(elements[i]);
      iter2.set(elements[i]);
      verify();
    }
  }

  /** Tests {@link List#add(int,Object)}. */
  public void testListAddByIndex() {
    if (!isAddSupported()) {
      return;
    }

    Object element = getOtherElements()[0];
    int max = getFullElements().length;

    for (int i = 0; i <= max; i++) {
      resetFull();
      ((List) collection).add(i, element);
      ((List) confirmed).add(i, element);
      verify();
    }
  }

  /** Tests {@link List#equals(Object)}. */
  public void testListEquals() {
    resetEmpty();
    List<@Nullable Object> list = getList();
    assertTrue("Empty lists should be equal", list.equals(confirmed));
    verify();
    assertTrue("Empty list should equal self", list.equals(list));
    verify();

    List<@Nullable Object> list2 = Arrays.asList(getFullElements());
    assertTrue("Empty list shouldn't equal full", !list.equals(list2));
    verify();

    list2 = Arrays.asList(getOtherElements());
    assertTrue("Empty list shouldn't equal other", !list.equals(list2));
    verify();

    resetFull();
    list = getList();
    assertTrue("Full lists should be equal", list.equals(confirmed));
    verify();
    assertTrue("Full list should equal self", list.equals(list));
    verify();

    list2 = makeEmptyList();
    assertTrue("Full list shouldn't equal empty", !list.equals(list2));
    verify();

    list2 = Arrays.asList(getOtherElements());
    assertTrue("Full list shouldn't equal other", !list.equals(list2));
    verify();

    list2 = Arrays.asList(getFullElements());
    Collections.reverse(list2);
    assertTrue(
        "Full list shouldn't equal full list with same elements" + " but different order",
        !list.equals(list2));
    verify();

    assertTrue("List shouldn't equal String", !list.equals(""));
    verify();

    final List listForC = Arrays.asList(getFullElements());
    Collection c =
        new AbstractCollection() {
          @Override
          public int size() {
            return listForC.size();
          }

          @Override
          public Iterator iterator() {
            return listForC.iterator();
          }
        };

    assertTrue(
        "List shouldn't equal nonlist with same elements " + " in same order", !list.equals(c));
    verify();
  }

  /** Tests {@link List#hashCode()}. */
  public void testListHashCode() {
    resetEmpty();
    int hash1 = collection.hashCode();
    int hash2 = confirmed.hashCode();
    assertEquals("Empty lists should have equal hashCodes", hash1, hash2);
    verify();

    resetFull();
    hash1 = collection.hashCode();
    hash2 = confirmed.hashCode();
    assertEquals("Full lists should have equal hashCodes", hash1, hash2);
    verify();
  }

  /** Tests {@link List#get(int)}. */
  public void testListGetByIndex() {
    resetFull();
    List list = getList();
    Object[] elements = getFullElements();
    for (int i = 0; i < elements.length; i++) {
      assertEquals("List should contain correct elements", elements[i], list.get(i));
      verify();
    }
  }

  /** Tests {@link List#indexOf()}. */
  public void testListIndexOf() {
    resetFull();
    List list1 = getList();
    List list2 = getConfirmedList();

    Iterator iterator = list2.iterator();
    while (iterator.hasNext()) {
      Object element = iterator.next();
      assertEquals(
          "indexOf should return correct result", list1.indexOf(element), list2.indexOf(element));
      verify();
    }

    Object[] other = getOtherElements();
    for (int i = 0; i < other.length; i++) {
      assertEquals("indexOf should return -1 for nonexistent element", -1, list1.indexOf(other[i]));
      verify();
    }
  }

  /** Tests {@link List#lastIndexOf()}. */
  public void testListLastIndexOf() {
    resetFull();
    List list1 = getList();
    List list2 = getConfirmedList();

    Iterator iterator = list2.iterator();
    while (iterator.hasNext()) {
      Object element = iterator.next();
      assertEquals(
          "lastIndexOf should return correct result",
          list1.lastIndexOf(element),
          list2.lastIndexOf(element));
      verify();
    }

    Object[] other = getOtherElements();
    for (int i = 0; i < other.length; i++) {
      assertEquals(
          "lastIndexOf should return -1 for nonexistent " + "element",
          -1,
          list1.lastIndexOf(other[i]));
      verify();
    }
  }

  /** Tests {@link List#remove(int)}. */
  public void testListRemoveByIndex() {
    if (!isRemoveSupported()) {
      return;
    }

    int max = getFullElements().length;
    for (int i = 0; i < max; i++) {
      resetFull();
      Object o1 = ((List) collection).remove(i);
      Object o2 = ((List) confirmed).remove(i);
      assertEquals("remove should return correct element", o1, o2);
      verify();
    }
  }

  /** Returns an empty {@link ArrayList}. */
  @Override
  protected Collection makeConfirmedCollection() {
    ArrayList list = new ArrayList();
    return list;
  }

  /** Returns a full {@link ArrayList}. */
  @Override
  protected Collection makeConfirmedFullCollection() {
    ArrayList list = new ArrayList();
    list.addAll(Arrays.asList(getFullElements()));
    return list;
  }

  /** Verifies that the test list implementation matches the confirmed list implementation. */
  @Override
  protected void verify() {
    super.verify();

    List list1 = getList();
    List list2 = getConfirmedList();

    assertEquals("List should equal confirmed", list1, list2);
    assertEquals("Confirmed should equal list", list2, list1);

    assertEquals("Hash codes should be equal", list1.hashCode(), list2.hashCode());

    int i = 0;
    Iterator iterator1 = list1.iterator();
    Iterator iterator2 = list2.iterator();
    Object[] array = list1.toArray();
    while (iterator2.hasNext()) {
      assertTrue("List iterator should have next", iterator1.hasNext());
      Object o1 = iterator1.next();
      Object o2 = iterator2.next();
      assertEquals("Iterator elements should be equal", o1, o2);
      o2 = list1.get(i);
      assertEquals("get should return correct element", o1, o2);
      o2 = array[i];
      assertEquals("toArray should have correct element", o1, o2);
      i++;
    }
  }
}
